/************************************************************************
Copyright 2003, 2004, 2005, 2006, 2007, 2008 Grid Technology Research Center,
National Institute of Advanced Industrial Science and Technology.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
************************************************************************/

package jp.aist.gtrc.plus.command;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Map.Entry;

import jp.aist.gtrc.plus.command.util.PlusReserveAccountOptions;
import jp.aist.gtrc.plus.command.util.PlusReserveCommand;
import jp.aist.gtrc.plus.command.util.SGEAccount;
import jp.aist.gtrc.plus.command.util.SGEAccount.SGEAccountKey;
import jp.aist.gtrc.plus.command.util.SGEAccount.SGEAccountData;
import jp.aist.gtrc.plus.reserve.ReserveException;
import jp.aist.gtrc.plus.reserve.ReserveId;
import jp.aist.gtrc.plus.reserve.ReserveLogger;
import jp.aist.gtrc.plus.reserve.ReserveStatus;
import jp.aist.gtrc.plus.scheduler.util.TimeUtil;

public class PlusReserveAccount extends PlusReserveCommand {

	interface ShowAccountFormatter {
		void showAccount(Collection<ReserveStatus> allStatus);
	}

	private static class Usage {
		private long duration;		// [msec]
		private double hourNode;	// used [hour * nodeNum]

		Usage() {
			this.duration = 0;
			this.hourNode = 0.0;
		}
		Usage(long duration, int nodeNum) {
			this.duration = duration;
			this.hourNode = TimeUtil.milliToHour(duration) * nodeNum;
		}
		Usage(long duration, double hourNode) {
			this.duration = duration;
			this.hourNode = hourNode;
		}

		void add(Usage newUsage) {
			if (newUsage != null) {
				this.duration += newUsage.duration;
				this.hourNode += newUsage.hourNode;
			}
		}
	}

	private final PerUsage allUsage = new PerUsage("ALL");

	private final class PerUsage {
		private final String keyName;
		private int useCount = 0;
		private final Usage reservedSum;
		private final Usage rsvUsedSum;
		private final Usage normUsedSum;

		private PerUsage(String keyName) {
			this.keyName = keyName;
			this.reservedSum = new Usage();
			this.rsvUsedSum = new Usage();
			this.normUsedSum = new Usage();
		}

		private void addUsage(PerUsage usage, Usage reserved,
				Usage rsvUsed, Usage normUsed) {
			if (reserved != null) {
				usage.useCount++;
			}
			usage.reservedSum.add(reserved);
			usage.rsvUsedSum.add(rsvUsed);
			usage.normUsedSum.add(normUsed);
		}

		private void addUsage(Usage reserved) {
			addUsage(this, reserved, null, null);
			addUsage(allUsage, reserved, null, null);
		}

		private void addUsage(Usage rsvUsed, Usage normUsed) {
			addUsage(this, null, rsvUsed, normUsed);
			addUsage(allUsage, null, rsvUsed, normUsed);
		}
	}

	protected abstract class PerFormatter implements ShowAccountFormatter {

		protected final PlusReserveAccountOptions options;
		protected final SGEAccount sgeAccount;
		private final boolean showNonRsv;

		protected final SGEAccount makeSGEAccount(
				PlusReserveAccountOptions options, SGEAccountKey keyType) {

			Calendar start, end;
			try {
				start = options.getStartTime();
				end = options.getEndTime();
				return new SGEAccount(start, end, keyType);
			} catch (ReserveException e) {
				return null;
			}

		}

		protected PerFormatter(PlusReserveAccountOptions options,
				SGEAccountKey keyType, boolean showNonRsv) {

			this.options = options;
			this.sgeAccount = makeSGEAccount(options, keyType);
			this.showNonRsv = showNonRsv;

		}

		private String ratio(double x, double y) {
			if (y != 0.0) {
				double z = (x / y);
				if (z < 10.0) {
					// 0.0% <= z <= 999.9%, show as percentage
					return String.format("%5.1f%%", 100.0 * z);
				} else {
					// z >= 1000.0%, show as ratio
					return String.format("%5.2f", z);
				}
			} else {
				return "---";
			}
		}

		protected String showDuration(long duration, long allDuration) {

			long dsec = duration / 1000;
			long sec = dsec % 60;
			long min = (dsec % 3600) / 60;
			long hour = (dsec / 3600) % 24;
			long day = dsec / (24 * 3600);
			String r = ratio(duration, allDuration);
			return String.format("%3dd+%02d:%02d:%02d(%s)", day, hour, min, sec, r);

		}

		protected String showHourNode(double hourNode) {

			return String.format("%.1f", hourNode);

		}

		protected void showUsage(PerUsage usage) {

			showLine(
				usage.keyName,
				Integer.toString(usage.useCount),
				showDuration(
						usage.reservedSum.duration,
						allUsage.reservedSum.duration),
				showDuration(
						usage.rsvUsedSum.duration,
						allUsage.rsvUsedSum.duration),
				showDuration(
						usage.normUsedSum.duration,
						allUsage.normUsedSum.duration),
				showHourNode(usage.reservedSum.hourNode),
				showHourNode(usage.rsvUsedSum.hourNode),
				showHourNode(usage.normUsedSum.hourNode),
				ratio(usage.rsvUsedSum.duration, usage.reservedSum.duration),
				ratio(usage.rsvUsedSum.duration, usage.normUsedSum.duration));

		}

		protected void showHeader(String keyName) {

			System.out.println(String.format("%63s%32s%7s%16s",
					"sum of duration", "rUsed", "rUsed", "hour*nodeNum"));
			showLine(keyName, "cnt",
					"reserved      ", "rsvUsed      ", "non-rsvUsed    ",
					"rsvd", "rUsed", "nUsed",
					"/rsvd", "/nUsed");

		}

		protected void showLine(String keyName, String count,
				String d1, String d2, String d3,
				String h1, String h2, String h3, String r1, String r2) {

			System.out.println(
				String.format(Locale.US,
					"%-18s %3s %21s %21s %21s %6s %6s %5s %5s %5s",
					keyName, count, d1, d2, d3, r1, r2, h1, h2, h3));

		}

		protected abstract String[] getMapKeys(ReserveStatus status);

		protected void addSGEUsage(SortedMap<String, PerUsage> map,
				Collection<ReserveStatus> allStatus) {

			for (PerUsage usage : map.values()) {
				String key = usage.keyName;
				Usage rsvU = new Usage(
						sgeAccount.getRsvDuration(key),
						sgeAccount.getRsvHourNode(key));
				Usage nonRsvU = new Usage(
						sgeAccount.getNonRsvDuration(key),
						sgeAccount.getNonRsvHourNode(key));
				usage.addUsage(rsvU, nonRsvU);
			}

		}

		protected boolean toBeAddedNonRsv(String key) {

			return false;

		}

		protected void addNonRsvUsage(SortedMap<String, PerUsage> map) {

			Collection<Entry<String, SGEAccountData>> set
				= sgeAccount.getNonRsvUsageMap().entrySet();
			for (Entry<String, SGEAccountData> entry : set) {
				String key = entry.getKey();
				if (toBeAddedNonRsv(key) == false) {
					continue;
				}
				PerUsage usage = map.get(key);
				if (usage == null) {
					usage = new PerUsage(key);
				}
				SGEAccountData data = entry.getValue();
				usage.addUsage(new Usage(0, 0.0),
						new Usage(data.getDuration(), data.getHourNode()));
				map.put(key, usage);
			}

		}

		protected Comparator<String> getComparator() {

			return new Comparator<String>() {
				public int compare(String o1, String o2) {
					return o1.compareTo(o2);
				}
			};
		}

		public void showAccount(String keyName, Collection<ReserveStatus> allStatus) {

			/*
			 * Make requested reserve duration map
			 */
			SortedMap<String, PerUsage> map
				= new TreeMap<String, PerUsage>(getComparator());
			for (ReserveStatus status : allStatus) {
				Long duration = TimeUtil.diffOfCal(
						status.getStartTime(), status.getEndTime());
				int nodeNum = status.getReservedNodeNames().length;
				for (String key : getMapKeys(status)) {
					PerUsage usage = map.get(key);
					if (usage == null) {
						usage = new PerUsage(key);
						map.put(key, usage);
					}
					usage.addUsage(new Usage(duration, nodeNum));
				}
			}

			// add really used reserve duration and non-reserve duration
			addSGEUsage(map, allStatus);

			// add non-reserve only user/node to map
			if (showNonRsv) {
				addNonRsvUsage(map);
			}

			/*
			 * show usage information
			 */
			showHeader(keyName);

			for (PerUsage usage : map.values()) {
				showUsage(usage);
			}
			if (map.size() > 1) {
				showUsage(allUsage);
			}

		}

	}

	class PerReserveFormatter extends PerFormatter {

		PerReserveFormatter(PlusReserveAccountOptions options) {
			super(options, SGEAccountKey.queue, false);
		}

		protected Comparator<String> getComparator() {

			return new Comparator<String>() {
				public int compare(String o1, String o2) {
					final int preLen = ReserveId.RSVID_PREFIX.length();
					try {
						int n1 = Integer.parseInt(o1.substring(preLen));
						int n2 = Integer.parseInt(o2.substring(preLen));
						return (n1 - n2);
					} catch (NumberFormatException e) {
						return -1;
					}
				}
			};
		}

		protected void showHeader(String keyName) {

			System.out.println(String.format("%37s%26s%5s",
					"sum of duration", "hour*nodeNum", "drtn"));
			showLine(keyName, "cnt",
					"reserved     ", "rsvUsed      ", "normUsed",
					"rsvd", "used", "norm",
					"used/rsvd", "normUsed");

		}

		protected void showLine(String keyName, String count,
				String d1, String d2, String d3,
				String h1, String h2, String h3, String r1, String r2) {

			// NOTE: ignore count, d3, h3, r2
			System.out.println(
					String.format(Locale.US,
							"%-6s %21s %21s %5s %5s %6s",
							keyName, d1, d2, h1, h2, r1));

		}

		protected String[] getMapKeys(ReserveStatus status) {
			return new String[]{ status.getReserveId().toString() };
		}

		public void showAccount(Collection<ReserveStatus> allStatus) {

			showAccount("rsvID", allStatus);
		}

	}

	class PerDateFormatter extends PerFormatter {

		private SGEAccount sgeDateAccount = null;

		PerDateFormatter(PlusReserveAccountOptions options) {
			super(options, SGEAccountKey.queue, false);
		}

		private Usage getNonRsvUsage(String key, Usage rsvUsage) {

			SGEAccountData totalData = sgeDateAccount.getTotalUsageMap().get(key);
			if (totalData != null) {
				return new Usage(totalData.getDuration() - rsvUsage.duration,
						totalData.getHourNode() - rsvUsage.hourNode);
			} else {
				return new Usage(0, 0.0);
			}
		}

		protected void addSGEUsage(SortedMap<String, PerUsage> map,
				Collection<ReserveStatus> allStatus) {

			HashMap<String, ReserveStatus> statusMap
				= new HashMap<String, ReserveStatus>();
			for (ReserveStatus status : allStatus) {
				statusMap.put(status.getReserveId().toString(), status);
			}

			Collection<Entry<String, SGEAccountData>> set
				= sgeAccount.getRsvUsageMap().entrySet();
			for (Entry<String, SGEAccountData> entry : set) {
				String qName = entry.getKey();
				SGEAccountData data = entry.getValue();
				ReserveStatus status = statusMap.get(qName);
				if (status == null) {
					continue;
				}
				String key = getMapKey(status);
				PerUsage usage = map.get(key);
				if (usage != null) {
					Usage rsvUsage
						= new Usage(data.getDuration(), data.getHourNode());
					usage.addUsage(rsvUsage, getNonRsvUsage(key, rsvUsage));
				}
			}

		}

		private String getMapKey(ReserveStatus status) {
			return String.format(Locale.US,
					SGEAccount.QACCT_DATE_FORMAT, status.getStartTime());
		}

		protected String[] getMapKeys(ReserveStatus status) {
			return new String[]{ getMapKey(status) };
		}

		public void showAccount(Collection<ReserveStatus> allStatus) {

			Calendar end = null;
			for (ReserveStatus status : allStatus) {
				if ((end == null) || end.before(status.getEndTime())) {
					end = status.getEndTime();
				}
			}
			options.setEndTime(end);
			sgeDateAccount = makeSGEAccount(options, SGEAccountKey.startDate);
			showAccount("reserve start", allStatus);

		}

	}

	class PerOwnerFormatter extends PerFormatter {

		PerOwnerFormatter(PlusReserveAccountOptions options) {
			super(options, SGEAccountKey.owner, true);
		}

		protected String[] getMapKeys(ReserveStatus status) {
			return new String[]{ status.getOwner() };
		}

		protected boolean toBeAddedNonRsv(String key) {

			String owner = options.getOwner();
			return (owner == null) ? true : owner.equals(key);

		}

		public void showAccount(Collection<ReserveStatus> allStatus) {

			showAccount("owner", allStatus);
		}

	}

	class PerNodeFormatter extends PerFormatter {

		private final String[] showNodes;

		PerNodeFormatter(PlusReserveAccountOptions options) {
			super(options, SGEAccountKey.node, true);
			this.showNodes = options.getReservedNodes();
		}

		protected boolean toBeAddedNonRsv(String key) {

			if (showNodes == null) {
				return true;
			} else {
				for (String node : showNodes) {
					if (node.equals(key)) {
						return true;
					}
				}
				return false;
			}

		}

		protected String[] getMapKeys(ReserveStatus status) {
			String[] rsvNodes = status.getReservedNodeNames();
			if (showNodes == null) {
				return rsvNodes;
			} else {
				ArrayList<String> array = new ArrayList<String>();
				for (String rsvNode : rsvNodes) {
					for (String showNode : showNodes) {
						if (rsvNode.equals(showNode)) {
							array.add(rsvNode);
						}
					}
				}
				return array.toArray(new String[array.size()]);
			}

		}

		public void showAccount(Collection<ReserveStatus> allStatus) {
			showAccount("node", allStatus);
		}

	}

	private ShowAccountFormatter makeFormatter(PlusReserveAccountOptions options) {

		if (options.toShowList())
			return new PerReserveFormatter(options);
		if (options.toShowPerNode())
			return new PerNodeFormatter(options);
		if (options.toShowPerStartDate())
			return new PerDateFormatter(options);
		return new PerOwnerFormatter(options);

	}

	private void exec(String[] args) throws Exception {

		PlusReserveAccountOptions options = new PlusReserveAccountOptions("plus_account");
		String owner = null;
		Calendar start = null;
		Calendar end = null;
		String[] nodes = null;

		try {
			options.parse(args);
			owner = options.getOwner();
			start = options.getStartTime();
			end = options.getEndTime();
			nodes = options.getReservedNodes();
		} catch (Exception e) {
			showCommandHelp(options, e);
			throw e;
		}

		try {
			ReserveLogger logger = new ReserveLogger(options.getLogFilePath());
			Collection<ReserveStatus> statusSet
				= logger.loadAll(owner, start, end, nodes);
			if (start == null) {
				options.setStartTime(logger.getLogStartedTime());
			}
			ShowAccountFormatter formatter = makeFormatter(options);
			formatter.showAccount(statusSet);
		} catch (Exception e) {
			showException(e);
			throw e;
		}

	}

	public static void main(String[] args) {

		try {
			PlusReserveAccount cmd = new PlusReserveAccount();
			cmd.exec(args);
		} catch (Exception e) {
			System.exit(1);
		}

	}

}
